/*
Calendar Widget for SensorDataBrowser
Derived from CalendarWidget for GWT http://gwt.components.googlepages.com/

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/ 
package org.hackystat.ui.sensordatabrowser.client;

import java.util.ArrayList;
import java.util.Date;

import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.ChangeListener;
import com.google.gwt.user.client.ui.ChangeListenerCollection;
import com.google.gwt.user.client.ui.ClickListener;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.DockPanel;
import com.google.gwt.user.client.ui.Grid;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.HasAlignment;
import com.google.gwt.user.client.ui.HorizontalPanel;
import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.PopupPanel;
import com.google.gwt.user.client.ui.SourcesChangeEvents;
import com.google.gwt.user.client.ui.Widget;

/**
 * The class provides a Calendar Widget for selecting dates and displaying sensor data information
 * by date.
 * @author David J. Nickles
 * @author Alexei Sokolov
 *
 */
public class CalendarWidget extends Composite 
                            implements ClickListener, SourcesChangeEvents {

  /**
   * This private class provides a calendar navigation bar.
   * @author David J. Nickles
   * @author Alexei Sokolov
   *
   */
  private class NavBar extends Composite implements ClickListener {

    public final DockPanel bar = new DockPanel();
    public final Button prevMonth = new Button("&lt;", this);
    public final Button prevYear = new Button("&lt;&lt;", this);
    public final Button nextYear = new Button("&gt;&gt;", this);
    public final Button nextMonth = new Button("&gt;", this);
    public final HTML title = new HTML();

    private final CalendarWidget calendar;

    /**
     * Create an initialize a navigation bar for the given calendar widget.
     * @param calendar a calendar widget to control
     */
    public NavBar(CalendarWidget calendar) {
      this.calendar = calendar;

      setWidget(bar);
      bar.setStyleName("navbar");
      title.setStyleName("header");

      HorizontalPanel prevButtons = new HorizontalPanel();
      prevButtons.add(prevMonth);
      prevButtons.add(prevYear);

      HorizontalPanel nextButtons = new HorizontalPanel();
      nextButtons.add(nextYear);
      nextButtons.add(nextMonth);

      bar.add(prevButtons, DockPanel.WEST);
      bar.setCellHorizontalAlignment(prevButtons, DockPanel.ALIGN_LEFT);
      bar.add(nextButtons, DockPanel.EAST);
      bar.setCellHorizontalAlignment(nextButtons, DockPanel.ALIGN_RIGHT);
      bar.add(title, DockPanel.CENTER);
      bar.setVerticalAlignment(DockPanel.ALIGN_MIDDLE);
      bar.setCellHorizontalAlignment(title, HasAlignment.ALIGN_CENTER);
      bar.setCellVerticalAlignment(title, HasAlignment.ALIGN_MIDDLE);
      bar.setCellWidth(title, "100%");
    }

    /**
     * Respond to clicks on the within the navigation bar.
     * @param sender the item clicked on.
     */
    public void onClick(Widget sender) {
      if (sender.equals(prevMonth)) {
        calendar.prevMonth();
      } 
      else if (sender.equals(prevYear)) {
        calendar.prevYear();
      } 
      else if (sender.equals(nextYear)) {
        calendar.nextYear();
      } 
      else if (sender.equals(nextMonth)) {
        calendar.nextMonth();
      }
    }
  }

  /**
   * A date widget within a calendar widget.
   * @author David J. Nickles
   * @author Alexei Sokolov
   *
   */
  private static class CellHTML extends HTML {
    private int day;
        
    /**
     * Creates and initializes a calendar cell.
     * @param text the information for this cell
     * @param day the day of the month that this cell will represent
     */
    public CellHTML(String text, int day) {
      super(text);
      this.day = day;
    }
        
    /**
     * The day that this cell represents.
     * @return the day of this cell
     */
    public int getDay() {
      return day;
    }
  }
  
  private final NavBar navbar = new NavBar(this);
  private final DockPanel outer = new DockPanel();

  private final Grid grid = new Grid(6, 7) {
    public boolean clearCell(int row, int column) {
      boolean retValue = super.clearCell(row, column);
            
      Element td = getCellFormatter().getElement(row, column);
      DOM.setInnerHTML(td, "");
      return retValue;
    }
  };

  private Date date = new Date();

  private ChangeListenerCollection changeListeners;
    
  private String[] days = new String[] 
  { "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" };

  private String[] months = new String[] 
  { "January", "February", "March", "April", "May", "June", "July", "August", "September", 
    "October", "November", "December" };
  
  private Session session;
  private PopupPanel waitPopup;
  
  private int lastMonth;

  /**
   * Create and initialize a calendar.
   * @param session parameters.
   */
  public CalendarWidget(Session session) {
    this.session = session;
    this.date = new Date();
    this.date.setHours(0);
    this.date.setMinutes(0);
    this.date.setSeconds(0);
    setWidget(outer);
    grid.setStyleName("table");
    grid.setCellSpacing(0);
    outer.add(navbar, DockPanel.NORTH);
    outer.add(grid, DockPanel.CENTER);
    setStyleName("CalendarWidget");
    this.waitPopup = new PopupPanel();
    this.waitPopup.add(new Image(Images.WAIT_IMAGE));

    drawCalendar(); // NOPMD
  }
  
  /**
   * Get the day-highlighting info from the servlet.
   */
  public void drawCalendar() {
    rpcGetOverview();
  }

  /**
   * Draws and highlight the calendar widget.
   * @param daysWithData list of days to highlight
   */
  public void drawCalendarContinue(ArrayList daysWithData) {
    int year = getYear();
    int month = getMonth();
    int day = getDay();
    setHeaderText(year, month);
    grid.getRowFormatter().setStyleName(0, "weekheader");
    for (int i = 0; i < days.length; i++) {
      grid.getCellFormatter().setStyleName(0, i, "days");
      grid.setText(0, i, days[i].substring(0, 3));
    }

    Date now = new Date();
    int sameDay = now.getDate(); 
    int today = (now.getMonth() == month && now.getYear() + 1900 == year) ? sameDay : 0;
        
    int firstDay = new Date(year - 1900, month, 1).getDay();
    int numOfDays = getDaysInMonth(year, month);

    int j = 0;
    for (int i = 1; i < 6; i++) {
      for (int k = 0; k < 7; k++, j++) {
        int displayNum = (j - firstDay + 1);
        if (j < firstDay || displayNum > numOfDays) {
          grid.getCellFormatter().setStyleName(i, k, "empty");
          grid.setHTML(i, k, "&nbsp;");
        } 
        else {
          HTML html = new CellHTML(
            "<span>" + displayNum + "</span>", 
            displayNum);
          html.addClickListener(this);
          grid.getCellFormatter().setStyleName(i, k, "cell");
          if (daysWithData != null && daysWithData.get(displayNum - 1) == Boolean.TRUE) {
            grid.getCellFormatter().addStyleName(i, k, "data");
          }
          if (displayNum == today) {
            grid.getCellFormatter().addStyleName(i, k, "today");
          } /* else if (displayNum == sameDay) {
            grid.getCellFormatter().addStyleName(i, k, "day");
          } */
          grid.setWidget(i, k, html);
        }
      }
    }
    session.getCompositeWidget().showStack(2);
  }

  /**
   * Display the month and year in the calendar header.
   * @param year to display
   * @param month to display
   */
  protected void setHeaderText(int year, int month) {
    navbar.title.setText(months[month] + ", " + year);
  }
    
  /**
   * Computer the number of days in the requested month.
   * @param year the requested year
   * @param month the request month
   * @return the days in the requested month.
   */
  private int getDaysInMonth(int year, int month) {
    int numDays = 0;
    switch (month) {
      case 1:
        if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0) {
          numDays = 29; // leap year
        } 
        else {
          numDays = 28;
        }
        break;
      case 3:
      case 5:
      case 8:
      case 10:
        numDays = 30;
        break;
      default:
        numDays = 31;
        break;
    }
    return numDays;
  }

  /**
   * Adjust the calendar to display the month prior to the current month.
   */
  public void prevMonth() {
    int month = getMonth() - 1;
    if (month < 0) {
      setDate(getYear() - 1, 11, getDay());
    } 
    else {
      setMonth(month);
    }
    drawCalendar();
  }

  /**
   * Adjust the calendar to display the month after the current month.
   */
  public void nextMonth() {
    int month = getMonth() + 1;
    if (month > 11) {
      setDate(getYear() + 1, 0, getDay());
    } 
    else {
      setMonth(month);
    }
    drawCalendar();
  }
    
  /**
   * Adjust the calendar to display the year before the currently displayed year.
   */
  public void prevYear() {
    setYear(getYear() - 1);
    drawCalendar();
  }

  /**
   * Adjust the calendar to display the year after the currently displayed year.
   */
  public void nextYear() {
    setYear(getYear() + 1);
    drawCalendar();
  }

  /**
   * Set the current date. 
   * @param year the year
   * @param month the month
   * @param day the day
   */
  private void setDate(int year, int month, int day) {
    this.date = new Date(year - 1900, month, day);
    this.date.setHours(0);
    this.date.setMinutes(0);
    this.date.setSeconds(0);
  }
    
  /**
   * Set the displayed year.
   * @param year to display
   */
  private void setYear(int year) {
    date.setYear(year - 1900);
  }

  /**
   * Set the displayed month.
   * @param month to display
   */
  private void setMonth(int month) {
    date.setMonth(month);
  }

  /**
   * The year that of the displayed month.
   * @return the year
   */
  public int getYear() {
    return 1900 + date.getYear();
  }

  /**
   * The month that is displayed.
   * @return the month of the year
   */
  public int getMonth() {
    return date.getMonth();
  }

  /**
   * The day of the displayed month.
   * @return the day of the month
   */
  public int getDay() {
    return date.getDate();
  }
    
  /**
   * Gets the selected Date as a date object.
   * @return the selected date.
   */
  public Date getDate() {
    return date;
  }
    
  /**
   * Returns the first of the month.
   * @return the first of the month as a date object.
   */
  public Date getFirstDayOfMonth() {
    Date day = (Date)date.clone();
    day.setDate(1);
    return day;
  }
    
  /**
   * the last date of the displayed month.
   * @return the displayed month as a date object, with the day set to the current dates day.
   */
  public Date getLastDayOfMonth() {
    Date day = (Date)date.clone();
    day.setDate(getDaysInMonth(day.getYear(), day.getMonth()));
    day.setHours(12);
    day.setMinutes(59);
    day.setSeconds(59);
    return day;
  }

  /**
   * This method responds to a click within the calendar widget.
   * @param sender the widget that was clicked
   */
  public void onClick(Widget sender) {
    CellHTML cell = (CellHTML)sender;
    this.lastMonth = date.getMonth();
    setDate(getYear(), getMonth(), cell.getDay());
    if (date.getMonth() != this.lastMonth) {
      drawCalendar();
    }
    if (changeListeners != null) {
      changeListeners.fireChange(this);
    }
  }

  /**
   * This method responds to an add change.
   * @param listener the item in which the addition occurred
   */
  public void addChangeListener(ChangeListener listener) {
    if (changeListeners == null) {
      changeListeners = new ChangeListenerCollection();
    }
    changeListeners.add(listener);
  }

  /**
   * This method responds to an remove change.
   * @param listener the item in which the remove occurred
   */
  public void removeChangeListener(ChangeListener listener) {
    if (changeListeners != null) {
      changeListeners.remove(listener);
    }
  }
  
  /**
   * This method sets the state of the days with data for the month.
   * @param daysWithData Boolean array represents the days with data
   * 
   * @gwt.typeArgs daysWithData <java.lang.Boolean>
   */
  public void colorCalendar(ArrayList daysWithData) {
    this.waitPopup.hide();
    drawCalendarContinue(daysWithData);
  }

  /**
   * This method performs an asynchronous remote call out to the sensor data browser service
   * to get a list of days with data.
   */ 
  public void rpcGetOverview() {
    if (getParent() != null) {
      final Widget parent = getParent();
      this.waitPopup.setPopupPositionAndShow(new PopupPanel.PositionCallback() {
        public void setPosition(int offsetWidth, int offsetHeight) {
          int left = ((parent.getAbsoluteLeft()) - offsetWidth) + 175;
          int top = ((parent.getAbsoluteTop())  - offsetHeight) + 125;
          waitPopup.setPopupPosition(left, top);
        }
      });
    }
    RPCServiceFactory.getInstance().getService().getOverview(this.session.getSensorbaseHost(),
      Session.DEFAULT, this.session.getUser(), this.session.getPassword(), 
      getYear(), getMonth() + 1,
      new AsyncCallback() 
      {
        public void onSuccess(Object result) {
          colorCalendar((ArrayList)result);
        }

        public void onFailure(Throwable caught) {
          if (caught instanceof com.google.gwt.user.client.rpc.InvocationException) {
            session.log(RPCServiceFactory.PRCERRORNAME, 
              "Cannot invoke rpcGetOverview method on servlet.");
          } 
          else {
            session.log(RPCServiceFactory.PRCERRORNAME, caught.toString());
          }
        }
      });
  }
}